#ifndef REPROJECTION_GLSL
#define REPROJECTION_GLSL


#define NORMAL_DISTANCE 0.1f
#define PLANE_DISTANCE 5.0f



bool PlaneDistanceDisocclusionCheck(vec3 currentPos, vec3 historyPos, vec3 currentNormal)
{
    vec3  toCurrent = currentPos - historyPos;
    float distToPlane = abs(dot(toCurrent, currentNormal));

    return distToPlane > PLANE_DISTANCE;
}



bool OutOfFrameDisocclusionCheck(ivec2 coord, ivec2 imageDim)
{
    // check whether reprojected pixel is inside of the screen
    if (any(lessThan(coord, ivec2(0, 0))) || any(greaterThan(coord, imageDim - ivec2(1, 1))))
        return true;
    else
        return false;
}



bool ObjectIndexDisocclusionCheck(float objectIndex, float prevObjectIndex)
{
    if (objectIndex == prevObjectIndex)
        return false;
    else
        return true;
}



bool NormalsDisocclusionCheck(vec3 currentNormal, vec3 historyNormal)
{
    if (pow(abs(dot(currentNormal, historyNormal)), 2) > NORMAL_DISTANCE)
        return false;
    else
        return true;
}



bool IsReprojectionValid(ivec2 coord, vec3 currentPos, vec3 historyPos, vec3 currentNormal, vec3 historyNormal, float currentObjectIndex, float historyObjectIndex, ivec2 imageDim)
{
    // check if the history sample is within the frame
    if (OutOfFrameDisocclusionCheck(coord, imageDim)) return false;

    // check if the history belongs to the same surface
    if (ObjectIndexDisocclusionCheck(currentObjectIndex, historyObjectIndex)) return false;

    // check if history sample is on the same plane
    if (PlaneDistanceDisocclusionCheck(currentPos, historyPos, currentNormal)) return false;

    // check normals for compatibility
    if (NormalsDisocclusionCheck(currentNormal, historyNormal)) return false;

    return true;
}



vec2 SurfacePointReprojection(ivec2 coord, vec2 motionVector, ivec2 size)
{
    return vec2(coord) + motionVector.xy * vec2(size);
}



vec2 VirtualPointReprojection(ivec2 currentCoord, ivec2 size, float depth, float rayLength, vec3 cameraPos, mat4 projViewInverse, mat4 prevProjView)
{
    const vec2 texCoord  = currentCoord / vec2(size);
    vec3 rayOrigin = WorldPositionFromDepth(texCoord, depth, projViewInverse);

    vec3 cameraRay = rayOrigin - cameraPos.xyz;

    float cameraRayLength = length(cameraRay);
    float reflectionRayLength = rayLength;

    cameraRay = normalize(cameraRay);

    vec3 parallaxHitPoint = cameraPos.xyz + cameraRay * (cameraRayLength + reflectionRayLength);

    vec4 reprojectedParallaxHitPoint = prevProjView * vec4(parallaxHitPoint, 1.0f);

    reprojectedParallaxHitPoint.xy /= reprojectedParallaxHitPoint.w;

    return (reprojectedParallaxHitPoint.xy * 0.5f + 0.5f) * vec2(size);
}



vec2 ComputeHistoryCoord(ivec2 currentCoord, ivec2 size, float depth, vec2 motion, float curvature, float rayLength, vec3 cameraPos, mat4 projViewInverse, mat4 prevProjView)
{
    const vec2 surfaceHistoryCoord = SurfacePointReprojection(currentCoord, motion, size);

    vec2 historyCoord = surfaceHistoryCoord;

    if (rayLength > 0.0f && curvature == 0.0f)
        historyCoord = VirtualPointReprojection(currentCoord, size, depth, rayLength, cameraPos, projViewInverse, prevProjView);

    return historyCoord;
}



bool ReProject(in ivec2 fragCoord, 
               in float depth, 
            #if defined(REPROJECTION_REFLECTIONS)
               in vec3 cameraPos,
            #endif               
               in mat4 projViewInverse,
            #if defined(REPROJECTION_REFLECTIONS)
               in mat4 prevProjView,
               in float rayLength,
            #endif   
               in sampler2D samplerGBuffer2,
               in sampler2D samplerGBuffer3,
               in sampler2D prevSamplerGBuffer2,
               in sampler2D prevSamplerGBuffer3,
               in sampler2D prevSamplerGBufferDepth,
               in sampler2D samplerHistoryOutput,
            #if defined(REPROJECTION_MOMENTS)               
               in sampler2D samplerHistoryMomentsLength,
            #else 
               in sampler2D samplerHistoryLength,
            #endif
            #if defined(REPROJECTION_SINGLE_COLOR_CHANNEL)
               out float historyColor,
            #else
               out vec3  historyColor, 
            #endif
            #if defined(REPROJECTION_MOMENTS)
               out vec2 historyMoments, 
            #endif
               out float historyLength)
{
    const vec2 imageDim = vec2(textureSize(samplerHistoryOutput, 0));
    const vec2 pixelCenter = vec2(fragCoord) + vec2(0.5f);
    const vec2 texCoord = pixelCenter / vec2(imageDim);

    const vec4 centerGBuffer2 = texelFetch(samplerGBuffer2, fragCoord, 0);
    const vec4 centerGBuffer3 = texelFetch(samplerGBuffer3, fragCoord, 0);

    const vec2 currentMotion = centerGBuffer2.zw;
    const vec3 currentNormal = OctohedralToDirection(centerGBuffer2.xy);
    const float currentObjectIndex = centerGBuffer3.z;
    const vec3 currentPos = WorldPositionFromDepth(texCoord, depth, projViewInverse);

#if defined(REPROJECTION_REFLECTIONS)
    const float curvature = centerGBuffer3.g;
    const vec2 historyTexCoord = texCoord + currentMotion;
    const vec2 reprojectedCoord = ComputeHistoryCoord(fragCoord, 
                                                         ivec2(imageDim), 
                                                         depth, 
                                                         currentMotion, 
                                                         curvature, 
                                                         rayLength, 
                                                         cameraPos, 
                                                         projViewInverse, 
                                                         prevProjView);   
    const ivec2 historyCoord = ivec2(reprojectedCoord);                                                      
    const vec2 historyCoordFloor = reprojectedCoord;                                                                                                         
#else 
    // +0.5 to account for texel center offset
    const ivec2 historyCoord = ivec2(vec2(fragCoord) + currentMotion.xy * imageDim + vec2(0.5f));
    const vec2 historyCoordFloor = floor(vec2(fragCoord)) + currentMotion.xy * imageDim;
    // const vec2 historyCoordFloor = currentMotion.xy * imageDim;
    const vec2 historyTexCoord = texCoord + currentMotion.xy;
#endif

#if defined(REPROJECTION_SINGLE_COLOR_CHANNEL)
    historyColor = 0.0f;
#else
    historyColor   = vec3(0.0f);
#endif
#if defined(REPROJECTION_MOMENTS)    
    historyMoments = vec2(0.0f);
#endif

    bool v[4];
    const ivec2 offset[4] = { ivec2(0, 0), ivec2(1, 0), ivec2(0, 1), ivec2(1, 1) };

    // check for all 4 taps of the bilinear filter for validity
    bool valid = false;
    for (int sampleIndex = 0; sampleIndex < 4; sampleIndex++)
    {
        ivec2 loc = ivec2(historyCoordFloor) + offset[sampleIndex];

        vec4 sampleGBuffer2 = texelFetch(prevSamplerGBuffer2, loc, 0);
        vec4 sampleGBuffer3 = texelFetch(prevSamplerGBuffer3, loc, 0);
        float sampleDepth = texelFetch(prevSamplerGBufferDepth, loc, 0).r;

        vec3 historyNormal = OctohedralToDirection(sampleGBuffer2.xy);
        float historyObjectIndex = sampleGBuffer3.z;
        vec3 historyPos = WorldPositionFromDepth(historyTexCoord, sampleDepth, projViewInverse);

        v[sampleIndex] = IsReprojectionValid(historyCoord, currentPos, historyPos, currentNormal, historyNormal, currentObjectIndex, historyObjectIndex, ivec2(imageDim));

        valid = valid || v[sampleIndex];
    }

    if (valid)
    {
        float sumw = 0;
        float x = fract(historyCoordFloor.x);
        float y = fract(historyCoordFloor.y);

        // bilinear weights
        float w[4] = { (1 - x) * (1 - y),
                       x * (1 - y),
                       (1 - x) * y,
                       x * y };

    #if defined(REPROJECTION_SINGLE_COLOR_CHANNEL)
        historyColor = 0.0f;
    #else
        historyColor = vec3(0.0f);
    #endif
    #if defined(REPROJECTION_MOMENTS)         
        historyMoments = vec2(0.0f);
    #endif

        // perform the actual bilinear interpolation
        for (int sampleIndex = 0; sampleIndex < 4; sampleIndex++)
        {
            ivec2 loc = ivec2(historyCoordFloor) + offset[sampleIndex];

            if (v[sampleIndex])
            {
            #if defined(REPROJECTION_SINGLE_COLOR_CHANNEL)
                historyColor += w[sampleIndex] * texelFetch(samplerHistoryOutput, loc, 0).r;
            #else
                historyColor += w[sampleIndex] * texelFetch(samplerHistoryOutput, loc, 0).rgb;
            #endif            
            #if defined(REPROJECTION_MOMENTS) 
                historyMoments += w[sampleIndex] * texelFetch(samplerHistoryMomentsLength, loc, 0).rg;
            #endif                
                sumw += w[sampleIndex];
            }
        }

        // redistribute weights in case not all taps were used
        valid = (sumw >= 0.01);
    #if defined(REPROJECTION_SINGLE_COLOR_CHANNEL)
        historyColor = valid ? historyColor / sumw : 0.0f;
    #else
        historyColor = valid ? historyColor / sumw : vec3(0.0f);
    #endif    
    #if defined(REPROJECTION_MOMENTS)         
        historyMoments = valid ? historyMoments / sumw : vec2(0.0f);
    #endif
    }
    if (!valid) // perform cross-bilateral filter in the hope to find some suitable samples somewhere
    {
        float cnt = 0.0;

        // this code performs a binary descision for each tap of the cross-bilateral filter
        const int radius = 1;
        for (int yy = -radius; yy <= radius; yy++)
        {
            for (int xx = -radius; xx <= radius; xx++)
            {
                ivec2 p = historyCoord + ivec2(xx, yy);

                vec4 sampleGBuffer2 = texelFetch(prevSamplerGBuffer2, p, 0);
                vec4 sampleGBuffer3 = texelFetch(prevSamplerGBuffer3, p, 0);
                float sampleDepth = texelFetch(prevSamplerGBufferDepth, p, 0).r;

                vec3 historyNormal = OctohedralToDirection(sampleGBuffer2.xy);
                float historyObjectIndex = sampleGBuffer3.z;
                vec3 historyPos = WorldPositionFromDepth(historyTexCoord, sampleDepth, projViewInverse);

                if (IsReprojectionValid(historyCoord, currentPos, historyPos, currentNormal, historyNormal, currentObjectIndex, historyObjectIndex, ivec2(imageDim)))
                {
                #if defined(REPROJECTION_SINGLE_COLOR_CHANNEL)
                    historyColor += texelFetch(samplerHistoryOutput, p, 0).r;
                #else
                    historyColor += texelFetch(samplerHistoryOutput, p, 0).rgb;
                #endif
                #if defined(REPROJECTION_MOMENTS) 
                    historyMoments += texelFetch(samplerHistoryMomentsLength, p, 0).rg;
                #endif                    
                    cnt += 1.0;
                }
            }
        }
        if (cnt > 0)
        {
            valid = true;
            historyColor /= cnt;
        #if defined(REPROJECTION_MOMENTS)             
            historyMoments /= cnt;
        #endif            
        }
    }

    if (valid)
    {
    #if defined(REPROJECTION_MOMENTS)        
        historyLength = texelFetch(samplerHistoryMomentsLength, historyCoord, 0).b;
    #else
        historyLength = texelFetch(samplerHistoryLength, historyCoord, 0).r;
    #endif        
    }    
    else
    {
    #if defined(REPROJECTION_SINGLE_COLOR_CHANNEL)
        historyColor = 0.0f;
    #else        
        historyColor = vec3(0.0f);
    #endif
    #if defined(REPROJECTION_MOMENTS)          
        historyMoments = vec2(0.0f);
    #endif
        historyLength = 0.0f;
    }

    return valid;
}



#endif